package org.wattdepot.modbus.sensor.shark;

import static org.wattdepot.datainput.DataInputClientProperty.WATTDEPOT_PASSWORD_KEY;
import static org.wattdepot.datainput.DataInputClientProperty.WATTDEPOT_URI_KEY;
import static org.wattdepot.datainput.DataInputClientProperty.WATTDEPOT_USERNAME_KEY;
import java.io.IOException;
import java.net.InetAddress;
import javax.xml.datatype.XMLGregorianCalendar;
import net.wimpi.modbus.core.ModbusException;
import net.wimpi.modbus.core.ModbusIOException;
import net.wimpi.modbus.core.ModbusSlaveException;
import net.wimpi.modbus.io.ModbusTCPTransaction;
import net.wimpi.modbus.msg.ReadMultipleRegistersRequest;
import net.wimpi.modbus.msg.ReadMultipleRegistersResponse;
import net.wimpi.modbus.net.TCPMasterConnection;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.wattdepot.client.BadXmlException;
import org.wattdepot.client.MiscClientException;
import org.wattdepot.client.NotAuthorizedException;
import org.wattdepot.client.ResourceNotFoundException;
import org.wattdepot.client.WattDepotClient;
import org.wattdepot.datainput.DataInputClientProperty;
import org.wattdepot.resource.property.jaxb.Property;
import org.wattdepot.resource.sensordata.jaxb.SensorData;
import org.wattdepot.resource.source.jaxb.Source;
import org.wattdepot.util.tstamp.Tstamp;

/**
 * Polls the SensorShark meter for power and energy data periodically, and sends the data to
 * WattDepot server.
 * 
 * @author Edwin Lim
 * 
 */
public class SensorShark {

  /** Name of the property file containing essential preferences. */
  protected DataInputClientProperty properties;
  /** The name of the source to be monitored. */
  private String sourceName;
  /** The rate at which to poll the source for new data. */
  private int updateRate;
  /** Whether to display debugging data. */
  private boolean debug;
  /** The hostname of the sensor to be monitored. */
  private String hostName;
  /** The port number of a meter. */
  private int modbusPort;

  /** Name of this tool. */
  private static final String toolName = "SensorShark";
  /** The default polling rate, in seconds. */
  private static final int DEFAULT_UPDATE_RATE = 10;
  /** The polling rate that indicates that it needs to be set to a default. */
  private static final int UPDATE_RATE_SENTINEL = 0;

  /** Making PMD happy. */
  private static final String REQUIRED_PARAMETER_ERROR_MSG =
      "Required parameter %s not found in properties.%n";

  /**
   * Creates a meter sensor for the Shark meter.
   * 
   * @param propertyFilename Name of the file to read essential properties from.
   * @param sourceName The name of the source to be monitored.
   * @param updateRate The rate at which to send new data to the source, in seconds.
   * @param modbusPort The modbus port to communicate with a meter.
   * @param debug If true then display new sensor data when sending it.
   * @param hostName The hostname of the shark sensor to be polled.
   * @throws IOException If the property file cannot be found or read.
   */
  public SensorShark(String propertyFilename, String sourceName, int updateRate, int modbusPort,
      boolean debug, String hostName) throws IOException {

    if (propertyFilename == null) {
      this.properties = new DataInputClientProperty();
    }
    else {
      this.properties = new DataInputClientProperty(propertyFilename);
    }
    this.sourceName = sourceName;
    if (updateRate < UPDATE_RATE_SENTINEL) {
      // Got a bogus updateRate, set to sentinel value to ensure a default is picked
      this.updateRate = UPDATE_RATE_SENTINEL;
    }
    else {
      this.updateRate = updateRate;
    }
    this.debug = debug;
    if (this.debug) {
      System.err.println(this.properties.echoProperties());
    }
    this.hostName = hostName;
    this.modbusPort = modbusPort;
  }

  /**
   * Creates bogus sensor data periodically and sends it to a Source in a WattDepot server.
   * 
   * @return False if there is a fatal problem. Otherwise will never return.
   * @throws InterruptedException If some other thread interrupts our sleep.
   */
  public boolean process() throws InterruptedException {
    String wattDepotURI = this.properties.get(WATTDEPOT_URI_KEY);
    if (wattDepotURI == null) {
      System.err.format(REQUIRED_PARAMETER_ERROR_MSG, WATTDEPOT_URI_KEY);
      return false;
    }
    String wattDepotUsername = this.properties.get(WATTDEPOT_USERNAME_KEY);
    if (wattDepotUsername == null) {
      System.err.format(REQUIRED_PARAMETER_ERROR_MSG, WATTDEPOT_USERNAME_KEY);
      return false;
    }
    String wattDepotPassword = this.properties.get(WATTDEPOT_PASSWORD_KEY);
    if (wattDepotPassword == null) {
      System.err.format(REQUIRED_PARAMETER_ERROR_MSG, WATTDEPOT_PASSWORD_KEY);
      return false;
    }

    String sourceURI = Source.sourceToUri(this.sourceName, wattDepotURI);

    WattDepotClient client =
        new WattDepotClient(wattDepotURI, wattDepotUsername, wattDepotPassword);
    Source source;
    if (client.isAuthenticated()) {
      try {
        source = client.getSource(this.sourceName);
      }
      catch (NotAuthorizedException e) {
        System.err.format("You do not have access to the source %s. " + "Aborting.%n",
            this.sourceName);
        return false;
      }
      catch (ResourceNotFoundException e) {
        System.err.format("Source %s does not exist on server. " + "Aborting.%n", this.sourceName);
        return false;
      }
      catch (BadXmlException e) {
        System.err.println("Received bad XML from server, which is weird. " + "Aborting.");
        return false;
      }
      catch (MiscClientException e) {
        System.err.println("Had problems retrieving source from server, "
            + "which is weird. Aborting.");
        return false;
      }
      if (this.updateRate == UPDATE_RATE_SENTINEL) {
        // Need to pick a reasonable default pollingInterval
        // Check the polling rate specified in the source
        String updateIntervalString = source.getProperty(Source.UPDATE_INTERVAL);
        if (updateIntervalString == null) {
          // no update interval, so just use hard coded default
          this.updateRate = DEFAULT_UPDATE_RATE;
        }
        else {
          int possibleInterval;
          try {
            possibleInterval = Integer.valueOf(updateIntervalString);
            if (possibleInterval > DEFAULT_UPDATE_RATE) {
              // Sane interval, so use it
              this.updateRate = possibleInterval;
            }
            else {
              // Bogus interval, so use hard coded default
              this.updateRate = DEFAULT_UPDATE_RATE;
            }
          }
          catch (NumberFormatException e) {
            System.err.println("Unable to parse updateInterval, " + "using default value: "
                + DEFAULT_UPDATE_RATE);
            // Bogus interval, so use hard coded default
            this.updateRate = DEFAULT_UPDATE_RATE;
          }
        }
      }

      SensorData data;
      while (true) {
        // Get data from Meter
        try {
          data = pollMeter(this.hostName, wattDepotPassword, sourceURI);
        }
        catch (IOException e1) {
          System.err.println("Unable to retrieve data from Meter, "
              + "hopefully this is temporary.");
          return false;
        }
        // Store SensorData in WattDepot server
        try {
          client.storeSensorData(data);
        }
        catch (Exception e) {
          System.out.println("Unable to store sensor data.");
        }
        if (debug) {
          System.out.println(data);
        }
        Thread.sleep(updateRate * 1000);
      }
    }
    else {
      System.err.format("Invalid credentials for source %s. " + "Aborting.", this.sourceName);
      return false;
    }
  }

  /**
   * Connects to Energy Meter, retrieves the latest data, and returns it as a SensorData object.
   * 
   * @param hostname The hostname of the Meter. Only the hostname, not URI.
   * @param toolName The name of the tool to be placed in the SensorData.
   * @param sourceURI The URI of the source (needed to create SensorData object).
   * @return The meter data as SensorData
   * @throws IOException If there are problems retrieving the data via HTTP.
   */
  private SensorData pollMeter(String hostname, String toolName, String sourceURI)
      throws IOException {

    // Jamod connections and transaction
    TCPMasterConnection con = null; // the connection
    ModbusTCPTransaction trans = null; // the transaction
    // Read the registers as Function 03 //
    ReadMultipleRegistersRequest req = null; // the request
    ReadMultipleRegistersResponse res = null; // the response
    // Assign command line args to variables
    InetAddress addr = InetAddress.getByName(hostname);

    // Variables for storing the parameters
    // int port = Modbus.DEFAULT_PORT;
    int port = modbusPort;
    // Registers for the Quadlogic Meter
    Integer[] registers = { 9018, 9019, 1506, 1507 };
    // the number of consecutive registers to read
    int count = 2;
    // power[0] = high bit
    // power[1] = low bit
    Integer[] power = new Integer[2];
    // energy[0] = high bit
    // energy[1] = low bit
    Integer[] energy = new Integer[2];
    // the reference; offset where to start reading from
    int ref = registers[2];

    // initialize arrays to 0
    for (int i = 0; i < power.length; i++) {
      power[i] = 0;
      energy[i] = 0;
    }

    // Open the connection
    con = new TCPMasterConnection(addr);
    con.setPort(port);
    try {
      con.connect();
    }
    catch (Exception e) {
      System.err.println("Could not connect to device: " + e.getMessage());
    }

    // Prepare the request
    req = new ReadMultipleRegistersRequest(ref, count);

    // Prepare the transaction
    trans = new ModbusTCPTransaction(con);
    trans.setRequest(req);

    // Execute the transaction repeat times
    int k = 0;
    // System.out.println("Connected... " + con.getAddress() +":" + port + "\n ");
    do {

      try {
        trans.execute();
      }
      catch (ModbusIOException e) {
        System.err.println("Could not load ModBus IO: " + e.getMessage());
      }
      catch (ModbusSlaveException e) {
        System.err.println("Could not Connect to the slave: " + e.getMessage());
      }
      catch (ModbusException e) {
        System.err.println("ModBus Exception: " + e.getMessage());
      }

      res = (ReadMultipleRegistersResponse) trans.getResponse();
      power[k] = res.getPowerEnergyValue(k);
      k++;
    }
    while (k < count);

    // Prepare the request
    ref = registers[0];
    req = new ReadMultipleRegistersRequest(ref, count);
    // Prepare the transaction
    trans.setRequest(req);

    // Execute the transaction repeat times
    k = 0;
    do {
      try {
        trans.execute();
      }
      catch (ModbusIOException e) {
        System.err.println("Could not load ModBus IO: " + e.getMessage());
      }
      catch (ModbusSlaveException e) {
        System.err.println("Could not Connect to the slave: " + e.getMessage());
      }
      catch (ModbusException e) {
        System.err.println("ModBus Exception: " + e.getMessage());
      }

      res = (ReadMultipleRegistersResponse) trans.getResponse();
      energy[k] = res.getPowerEnergyValue(k);
      k++;
    }
    while (k < count);

    XMLGregorianCalendar timestamp = Tstamp.makeTimestamp();
    // Calculations reflect results on the Acuvim software
    Float currentPower = Float.intBitsToFloat(power[0] + power[1]);
    Float floatEnergy = Float.intBitsToFloat(energy[0] + energy[1]);
    currentPower = currentPower / 10;
    System.out.println("Power: " + currentPower + " W\nEnergy: " + floatEnergy
        + " wh\nCurrent Time: " + timestamp);
    // Close the Modbus connection
    con.close();

    SensorData data = new SensorData(timestamp, toolName, sourceURI);
    data.addProperty(new Property(SensorData.POWER_CONSUMED, currentPower));
    data.addProperty(new Property(SensorData.ENERGY_CONSUMED_TO_DATE, floatEnergy));
    return data;
  }

  /**
   * Processes command line arguments, creates the ExampleStreamingSensor object and starts sending.
   * 
   * @param args command line arguments.
   * @throws InterruptedException If some other thread interrupts our sleep.
   */
  public static void main(String[] args) throws InterruptedException {
    Options options = new Options();
    options.addOption("h", "help", false, "Print this message");
    options.addOption("p", "propertyFilename", true, "Filename of property file");
    options.addOption("s", "source", true, "Name of the source "
        + "to send data to, ex. \"foo-source\"");
    options.addOption("u", "updateRate", true, "The rate at which to query the meter, in seconds. "
        + "If not specified, will " + "default to value of source's updateInterval property, or "
        + DEFAULT_UPDATE_RATE + " seconds if source has no such property");
    options.addOption("m", "hostName", true,
        "Hostname of the Meter gateway. Note just hostname, not full URI");
    options.addOption("n", "port", true, "Port of the Meter gateway.");
    options
        .addOption("d", "debug", false, "Displays sensor data as it " + "is sent to the server.");

    CommandLine cmd = null;
    String propertyFilename = null, sourceName = null, hostName = null, wattDepotURI = null, 
      userName = null, password = null;
    int modbusPort = 502;
    int updateRate = UPDATE_RATE_SENTINEL;
    boolean debug = false;

    CommandLineParser parser = new PosixParser();
    HelpFormatter formatter = new HelpFormatter();
    try {
      cmd = parser.parse(options, args);
    }
    catch (ParseException e) {
      System.err.println("Command line parsing failed. Reason: " + e.getMessage() + ". Exiting.");
      System.exit(1);
    }

    if (cmd.hasOption("h")) {
      formatter.printHelp(toolName, options);
      System.exit(0);
    }
    if (cmd.hasOption("p")) {
      propertyFilename = cmd.getOptionValue("p");
    }
    else {
      System.out.println("No property file name provided, using default.");
    }
    if (cmd.hasOption("s")) {
      sourceName = cmd.getOptionValue("s");
    }
    else {
      System.err.println("Required Source name parameter not provided, exiting.");
      formatter.printHelp(toolName, options);
      System.exit(1);
    }
    if (cmd.hasOption("n")) {
      String modbusPortString = cmd.getOptionValue("n");
      try {
        modbusPort = Integer.parseInt(modbusPortString);
      }
      catch (NumberFormatException e) {
        System.err.println("Provided updateRate does not appear " + "to be a number. Bad format?");
        formatter.printHelp(toolName, options);
        System.exit(1);
      }
    }
    else {
      System.err.println("Required port number parameter not provided, " + "exiting.");
      formatter.printHelp(toolName, options);
      System.exit(1);
    }
    if (cmd.hasOption("u")) {
      String updateRateString = cmd.getOptionValue("u");
      // Make an int out of the command line argument
      try {
        updateRate = Integer.parseInt(updateRateString);
      }
      catch (NumberFormatException e) {
        System.err.println("Provided updateRate does not appear to " + "be a number. Bad format?");
        formatter.printHelp(toolName, options);
        System.exit(1);
      }
      // Check rate for sanity
      if (updateRate < 1) {
        System.err.println("Provided updateRate is < 1, which is not allowed.");
        formatter.printHelp(toolName, options);
        System.exit(1);
      }
    }
    if (cmd.hasOption("m")) {
      hostName = cmd.getOptionValue("m");
    }
    else {
      System.err.println("Required tedHostname parameter not provided, exiting.");
      formatter.printHelp(toolName, options);
      System.exit(1);
    }

    debug = cmd.hasOption("d");

    if (debug) {
      System.out.println("propertyFilename: " + propertyFilename);
      System.out.println("wattDepotURI: " + wattDepotURI);
      System.out.println("userName: " + userName);
      System.out.println("password: " + password);
      System.out.println("sourceName: " + sourceName);
      System.out.println("updateRate: " + updateRate);
      System.out.println("hostName: " + hostName);
      System.out.println("modbusPort: " + modbusPort);
      System.out.println("debug: " + debug);
      System.out.println();
    }

    // Actually create the input client
    SensorShark sensor = null;
    try {
      sensor =
          new SensorShark(propertyFilename, sourceName, updateRate, modbusPort, 
              debug, hostName);
    }
    catch (IOException e) {
      System.err.println("Unable to read properties file, terminating.");
      System.exit(2);
    }
    // Just do it
    if (sensor != null) {
      sensor.process();
    }
  }

}
